OptionsBasedMetricRecordingFilter Class
Namespace: Diginsight.Diagnostics
Assembly: Diginsight.Diagnostics.dll
Configuration-based filter for controlling which activities record span duration metrics, using pattern matching rules defined in application settings.
public class OptionsBasedMetricRecordingFilter : IMetricRecordingFilterInheritance
Object ? OptionsBasedMetricRecordingFilter
Implements
IMetricRecordingFilter
Summary
The OptionsBasedMetricRecordingFilter class enables declarative filtering of metric recording through configuration files (e.g., appsettings.json). It uses pattern matching against activity source names and operation names to determine whether metrics should be recorded, providing fine-grained control over which operations generate telemetry data without code changes.
Key capabilities: - ? Configuration-driven filtering - control metric recording via appsettings.json - ? Pattern matching - use wildcards (*) and pipe separators (|) for flexible matching - ? Instrument-specific options - different rules per metric instrument - ? Hierarchical evaluation - instrument-specific rules override general rules - ? All-or-nothing logic - all matching patterns must agree to record - ? Virtualizable method - easily extensible for custom filtering logic
Constructors
OptionsBasedMetricRecordingFilter(IOptionsMonitor)
Initializes a new instance of the OptionsBasedMetricRecordingFilter class.
public OptionsBasedMetricRecordingFilter(
IOptionsMonitor<OptionsBasedMetricRecordingFilterOptions> filterMonitor
)Parameters
filterMonitor : IOptionsMonitor<OptionsBasedMetricRecordingFilterOptions>
Monitor for accessing filter configuration options. Uses IOptionsMonitor to support dynamic configuration changes at runtime without restarting the application.
Remarks
The constructor stores the options monitor which allows: - Access to named options for instrument-specific configurations - Hot reload support - configuration changes apply immediately - Default configuration via CurrentValue property
Methods
ShouldRecord(Activity, Instrument)
Determines whether a metric should be recorded for the specified activity and instrument.
public virtual bool? ShouldRecord(Activity activity, Instrument instrument)Returns
bool?
- true - Record the metric (all matching patterns returned true) - false - Skip recording (no patterns matched, or matched patterns didn’t all agree) - null - Not applicable (defers to other filters or default configuration)
Parameters
activity : Activity
The activity being evaluated for metric recording.
instrument : Instrument
The metric instrument that would record the measurement (e.g., histogram for span duration).
Remarks
This method implements a two-tier filtering strategy:
- Instrument-specific filtering: First checks configuration named by
instrument.Name - General filtering: Falls back to default (unnamed) configuration if no specific rules match
Evaluation logic:
// Step 1: Try instrument-specific rules
var specificMatches = GetMatches(filterMonitor.Get(instrument.Name));
if (specificMatches.Any())
return specificMatches.All(x => x); // All must be true
// Step 2: Try general rules
var generalMatches = GetMatches(filterMonitor.CurrentValue);
return generalMatches.Any() && generalMatches.All(x => x); // At least one, all must be truePattern matching: - Extracts activity.Source.Name and activity.OperationName - Compares against configured patterns using ActivityUtils.FullNameMatchesPattern - If multiple patterns match, all must return the same value (true or false)
Return value determination: - If any patterns match for instrument-specific config ? returns true only if all matched patterns are true - Otherwise, if any patterns match for general config ? returns true only if all matched patterns are true - If no patterns match ? returns false
Example
Configuration:
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.Orders.*": true,
"MyApp.Database.*": true,
"System.*": false
}
},
"diginsight.span_duration": {
"ActivityNames": {
"MyApp.Orders.SlowOperation": false,
"MyApp.*": true
}
}
}Filter behavior:
// Activity: Source="MyApp.Orders", Operation="ProcessOrder"
// Instrument: Name="diginsight.span_duration"
// Result: true (matches "MyApp.*" in specific config)
// Activity: Source="MyApp.Orders", Operation="SlowOperation"
// Instrument: Name="diginsight.span_duration"
// Result: false (matches specific pattern "MyApp.Orders.SlowOperation": false)
// Activity: Source="System.Net.Http", Operation="HttpRequest"
// Instrument: Name="diginsight.span_duration"
// Result: false (matches "System.*": false in general config)
// Activity: Source="ThirdParty.Library", Operation="DoWork"
// Instrument: Name="custom.metric"
// Result: false (no patterns match)Pattern Syntax
Basic Patterns
Pattern matching is performed by ActivityUtils.FullNameMatchesPattern and supports:
Operation Name Only
Match by operation name alone:
{
"ActivityNames": {
"ProcessOrder": true, // Exact match
"Process*": true, // Starts with "Process"
"*Order": true, // Ends with "Order"
"*Process*": true // Contains "Process"
}
}Full Name Pattern (Source|Operation)
Match by source name AND operation name using pipe separator:
{
"ActivityNames": {
"MyApp.Orders|ProcessOrder": true, // Exact source and operation
"MyApp.*|Process*": true, // Wildcard source and operation
"MyApp.Orders|": true, // Any operation in MyApp.Orders
"|ProcessOrder": true // ProcessOrder in any source
}
}Wildcard Rules
Single wildcard (*): - At the beginning: matches any prefix (*Order matches ProcessOrder, CreateOrder) - At the end: matches any suffix (Process* matches ProcessOrder, ProcessPayment) - Both sides: matches if the middle part is contained (*Process* matches GetProcessStatus) - Alone: matches everything (* matches all)
Multiple wildcards: - Not supported - pattern parsing throws ArgumentException - Use multiple pattern entries instead
Case sensitivity: - Pattern matching is case-insensitive
Pattern Precedence
When multiple patterns match the same activity:
- Instrument-specific patterns (named options) take precedence over general patterns
- All matching patterns must return the same value (
trueorfalse) - If patterns conflict (some
true, somefalse), the result is determined by theAll()logic
Example of conflicting patterns:
{
"ActivityNames": {
"MyApp.Orders.*": true,
"MyApp.Orders.SlowOperation": false
}
}For MyApp.Orders.SlowOperation: - Both patterns match - One returns true, one returns false - All(x => x) returns false (not all are true) - Result: false (don’t record)
Configuration
OptionsBasedMetricRecordingFilterOptions
Configure the filter in appsettings.json:
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"<pattern>": true|false
}
}
}Properties
ActivityNames : IDictionary<string, bool>
Dictionary mapping activity name patterns to recording decisions: - Key: Pattern string (supports wildcards and pipe separator) - Value: true to record metrics, false to skip
Named Configuration (Instrument-Specific)
Configure different rules for specific metric instruments:
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.*": true
}
},
"diginsight.span_duration": {
"ActivityNames": {
"MyApp.Orders.*": true,
"MyApp.Inventory.*": false
}
},
"custom.metric.name": {
"ActivityNames": {
"MyApp.Critical.*": true
}
}
}How it works: - Section name matches instrument.Name parameter - Instrument-specific rules evaluated first - Falls back to OptionsBasedMetricRecordingFilter section if no specific rules match
Usage Examples
Basic Registration
Register the filter during application startup:
var builder = WebApplication.CreateBuilder(args);
// Register filter
builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();
// Register metric recorder (uses the filter)
builder.Services.AddSpanDurationMetricRecorder();
var app = builder.Build();
app.Run();Simple Configuration
appsettings.json:
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.Orders.*": true,
"MyApp.Payment.*": true,
"MyApp.Inventory.CheckAvailability": true,
"Microsoft.AspNetCore.*": false,
"System.*": false
}
}
}Result: - ? Records: MyApp.Orders.ProcessOrder, MyApp.Payment.Charge - ? Records: MyApp.Inventory.CheckAvailability - ? Skips: Microsoft.AspNetCore.Hosting.HttpRequestIn - ? Skips: System.Net.Http.HttpRequestOut
Advanced Pattern Matching
appsettings.json:
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.*|Process*": true, // Any Process* operation in MyApp.*
"MyApp.*|*Slow*": false, // Skip operations containing "Slow"
"ThirdParty.Library|ImportantOp": true, // Specific operation in specific source
"*|HealthCheck": false, // HealthCheck in any source
"MyApp.Background.*": false // All background operations
}
}
}Test cases:
// MyApp.Orders | ProcessOrder ? true (matches "MyApp.*|Process*")
// MyApp.Orders | ProcessSlowOrder ? false (matches "*|*Slow*")
// MyApp.Orders | CreateOrder ? false (no match)
// ThirdParty.Library | ImportantOp ? true (exact match)
// ThirdParty.Library | OtherOp ? false (no match)
// MyApp.Health | HealthCheck ? false (matches "*|HealthCheck")Instrument-Specific Configuration
Different rules for different metrics:
appsettings.json:
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.*": true
}
},
"diginsight.span_duration": {
"ActivityNames": {
"MyApp.Orders.SlowOperation": false,
"MyApp.Orders.*": true
}
},
"custom.request_count": {
"ActivityNames": {
"MyApp.*": true,
"ThirdParty.*": true
}
}
}Behavior:
// For span_duration instrument:
// MyApp.Orders.ProcessOrder ? true
// MyApp.Orders.SlowOperation ? false (specific exclusion)
// MyApp.Inventory.CheckStock ? false (not matched in specific config, no general match)
// For custom.request_count instrument:
// MyApp.Orders.ProcessOrder ? true
// ThirdParty.Library.Execute ? true
// For other instruments:
// MyApp.Orders.ProcessOrder ? true (general config)Multiple Filters
Combine with other filters for complex scenarios:
builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();
builder.Services.AddSingleton<IMetricRecordingFilter, HttpHeadersSpanDurationMetricRecordingFilter>();
builder.Services.AddSingleton<IMetricRecordingFilter, CustomBusinessLogicFilter>();Filter execution order: - All registered filters are evaluated - If any filter returns non-null, that result takes precedence - SpanDurationMetricRecorder uses the filter result with ?? fallback to configuration
Hot Reload Configuration
Changes to appsettings.json apply immediately without restart:
// Before (recording Orders)
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.Orders.*": true
}
}
}
// After hot reload (no longer recording Orders)
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.Orders.*": false,
"MyApp.Payment.*": true
}
}
}Implementation: - Uses IOptionsMonitor<T> for reactive configuration - No application restart required - Changes apply to next activity evaluation
Custom Derived Filter
Extend for custom logic:
public class BusinessRulesMetricFilter : OptionsBasedMetricRecordingFilter
{
private readonly IFeatureFlagService _featureFlags;
public BusinessRulesMetricFilter(
IOptionsMonitor<OptionsBasedMetricRecordingFilterOptions> filterMonitor,
IFeatureFlagService featureFlags)
: base(filterMonitor)
{
_featureFlags = featureFlags;
}
public override bool? ShouldRecord(Activity activity, Instrument instrument)
{
// Check feature flag first
if (!_featureFlags.IsEnabled("DetailedMetrics"))
return false;
// Fallback to configuration-based filtering
return base.ShouldRecord(activity, instrument);
}
}Registration:
builder.Services.AddSingleton<IMetricRecordingFilter, BusinessRulesMetricFilter>();Performance Considerations
Pattern Matching Performance
Efficiency: - Pattern matching uses string.StartsWith() and string.EndsWith() - Case-insensitive comparison has minimal overhead - Frozen options prevent modification during evaluation
Optimization tips: - ? Use fewer, broader patterns over many specific patterns - ? Place most common patterns first in configuration - ? Avoid excessive wildcard patterns that match everything
Memory Efficiency
Configuration freezing:
((IOptionsBasedMetricRecordingFilterOptions)options.Freeze())- Options are frozen to immutable collections before evaluation
- Prevents modification during concurrent access
- Creates immutable dictionary copy only once per instrument
No allocations during filtering: - LINQ operations use ToArray() to avoid multiple enumerations - Pattern matching uses stack-allocated spans where possible
Early Exit Optimization
Instrument-specific rules:
IEnumerable<bool> specificMatches = GetMatches(filterMonitor.Get(instrument.Name));
if (specificMatches.Any())
return specificMatches.All(static x => x);- If instrument-specific rules match, general rules are never evaluated
- Reduces configuration lookups by ~50% for named instruments
Thread Safety
The OptionsBasedMetricRecordingFilter is thread-safe:
- ?
IOptionsMonitor<T>is thread-safe by design - ?
Freeze()creates immutable snapshot for evaluation - ? No mutable state between filter evaluations
- ? Pattern matching logic is stateless
Multiple threads can evaluate different activities concurrently without synchronization issues.
Troubleshooting
Metrics Not Being Filtered
Symptoms: All activities record metrics despite filter configuration.
Checklist: 1. ? Is the filter registered in DI container? csharp builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();
? Is the configuration section correctly named?
"OptionsBasedMetricRecordingFilter": { ... }? Are patterns matching the expected activities?
// Debug: check activity properties Console.WriteLine($"Source: {activity.Source.Name}"); Console.WriteLine($"Operation: {activity.OperationName}");? Are multiple filters registered? Check precedence order.
Pattern Not Matching
Symptoms: Expected pattern doesn’t match activities.
Common issues:
? Wrong separator:
// Wrong - using colon
"MyApp.Orders:ProcessOrder": true
// Correct - using pipe
"MyApp.Orders|ProcessOrder": true? Case mismatch assumptions:
// Unnecessary - matching is case-insensitive
"myapp.orders.*": true // Works the same as "MyApp.Orders.*"? Multiple wildcards:
// Invalid - throws ArgumentException
"MyApp.*.Orders.*": true
// Valid - single wildcard
"MyApp.*": trueDebugging tool:
var result = ActivityUtils.FullNameMatchesPattern(
"MyApp.Orders", // source name
"ProcessOrder", // operation name
"MyApp.Orders|Process*" // pattern
);
// Returns: trueUnexpected Filter Results
Symptoms: Activities recording/not recording contrary to expectations.
Cause 1: Multiple patterns conflict
{
"ActivityNames": {
"MyApp.*": true,
"MyApp.Orders.SlowOp": false
}
}For MyApp.Orders.SlowOp: - Both patterns match - All(x => x) evaluates to false (not all are true) - Result: false (won’t record)
Solution: Be explicit with more specific patterns, or use exclusions only.
Cause 2: Instrument-specific overrides general
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": { "MyApp.*": true }
},
"diginsight.span_duration": {
"ActivityNames": { "MyApp.Orders.*": false }
}
}For MyApp.Orders.ProcessOrder with diginsight.span_duration instrument: - Checks instrument-specific config first - Matches "MyApp.Orders.*": false - Result: false (never checks general config)
Solution: Understand the two-tier evaluation strategy.
Performance Impact
Symptoms: High CPU usage or latency when recording metrics.
Diagnosis:
// Check how many patterns are evaluated
var options = serviceProvider.GetService<IOptions<OptionsBasedMetricRecordingFilterOptions>>();
Console.WriteLine($"Pattern count: {options.Value.ActivityNames.Count}");Mitigation: - Reduce number of configured patterns - Use broader patterns (e.g., MyApp.* instead of listing each namespace) - Consider custom filter with caching for complex scenarios
Design Patterns
Strategy Pattern
The filter implements the Strategy pattern: - IMetricRecordingFilter defines the strategy interface - OptionsBasedMetricRecordingFilter is one concrete strategy - SpanDurationMetricRecorder uses the strategy without knowing implementation details
Options Pattern
Uses the .NET Options pattern: - IOptionsMonitor<T> for reactive configuration - Named options for instrument-specific configuration - Hot reload support without restart
Template Method Pattern
Virtual ShouldRecord method enables customization:
public class CustomFilter : OptionsBasedMetricRecordingFilter
{
public override bool? ShouldRecord(Activity activity, Instrument instrument)
{
// Custom pre-processing
// ...
// Call base implementation
return base.ShouldRecord(activity, instrument);
}
}Best Practices
Configuration Organization
? DO group related patterns:
{
"ActivityNames": {
"MyApp.Orders.*": true,
"MyApp.Payment.*": true,
"MyApp.Shipping.*": true,
"System.*": false,
"Microsoft.*": false
}
}? DO use instrument-specific sections for special cases:
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": { "MyApp.*": true }
},
"expensive.custom.metric": {
"ActivityNames": { "MyApp.Critical.*": true }
}
}? DON’T create overly complex pattern hierarchies:
{
"ActivityNames": {
"MyApp.*": true,
"MyApp.Orders.*": false,
"MyApp.Orders.Critical.*": true,
"MyApp.Orders.Critical.Slow*": false
}
}Pattern Design
? DO start with broad patterns and add exceptions:
{
"ActivityNames": {
"MyApp.*": true,
"MyApp.Internal.*": false
}
}? DO explicitly exclude noisy framework activities:
{
"ActivityNames": {
"Microsoft.AspNetCore.*": false,
"System.Net.Http.*": false
}
}? DON’T mix inclusion and exclusion without clear precedence:
{
"ActivityNames": {
"MyApp.*": true,
"MyApp.Orders.*": false,
"MyApp.Orders.Special": true // Confusing!
}
}Testing Filters
[Fact]
public void ShouldRecord_MatchesPattern_ReturnsTrue()
{
// Arrange
var options = new OptionsBasedMetricRecordingFilterOptions
{
ActivityNames = { ["MyApp.Orders.*"] = true }
};
var monitor = Mock.Of<IOptionsMonitor<OptionsBasedMetricRecordingFilterOptions>>(
m => m.CurrentValue == options && m.Get(It.IsAny<string>()) == new OptionsBasedMetricRecordingFilterOptions());
var filter = new OptionsBasedMetricRecordingFilter(monitor);
var activitySource = new ActivitySource("MyApp.Orders");
using var activity = activitySource.StartActivity("ProcessOrder");
var instrument = Mock.Of<Instrument>(i => i.Name == "test.metric");
// Act
var result = filter.ShouldRecord(activity, instrument);
// Assert
Assert.True(result);
}Version History
| Version | Changes |
|---|---|
| 3.0.0 | Initial release with IMetricRecordingFilter support |
| 3.1.0 | Added instrument-specific named configuration support |
| 3.2.0 | Improved pattern matching performance with frozen options |
See Also
- How Metric Recording Works with Diginsight and OpenTelemetry
- IMetricRecordingFilter Interface
- OptionsBasedMetricRecordingFilterOptions Class
- SpanDurationMetricRecorder Class
- HttpHeadersSpanDurationMetricRecordingFilter Class
- ActivityUtils Class
Remarks
The OptionsBasedMetricRecordingFilter provides a declarative, configuration-driven approach to metric filtering that enables operations teams to control telemetry costs without code changes. By leveraging pattern matching and hierarchical configuration, it offers both simplicity for common scenarios and flexibility for complex filtering requirements.
Design principles: - ?? Configuration over code - change behavior via settings, not deployments - ? Performance-optimized - minimal overhead with frozen options and early exit - ?? Flexible pattern matching - wildcards and source|operation syntax - ?? Instrument-aware - different rules for different metrics - ?? Hot reload - configuration changes apply immediately - ?? Extensible - virtual method enables custom subclasses
Common use cases: - Filter out noisy framework activities to reduce costs - Record metrics only for business-critical operations - Different sampling rates per metric type - Environment-specific filtering (dev vs. production) - Temporary metric collection for troubleshooting
Integration:
builder.Services
.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>()
.AddSpanDurationMetricRecorder();This configuration-based approach aligns with the “Infrastructure as Configuration” principle, making telemetry behavior observable and version-controlled through application settings.